/*
 * Copyright (c) godot-rust; Bromeon and contributors.
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

use crate::generator::functions_common;
use crate::generator::functions_common::{FnCode, FnParamTokens};
use crate::models::domain::{FnParam, FnQualifier, Function, RustTy, TyName};
use crate::util::{ident, safe_ident};
use crate::{conv, special_cases};
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote, ToTokens};

pub fn make_function_definition_with_defaults(
    sig: &dyn Function,
    code: &FnCode,
    full_fn_name: &Ident,
    cfg_attributes: &TokenStream,
) -> (TokenStream, TokenStream) {
    let (default_fn_params, required_fn_params): (Vec<_>, Vec<_>) = sig
        .params()
        .iter()
        .partition(|arg| arg.default_value.is_some());

    let simple_fn_name = safe_ident(sig.name());
    let extended_fn_name = format_ident!("{}_ex", simple_fn_name);
    let default_parameter_usage = format!("To set the default parameters, use [`Self::{}`] and its builder methods.  See [the book](https://godot-rust.github.io/book/godot-api/functions.html#default-parameters) for detailed usage instructions.", extended_fn_name);
    let vis = functions_common::make_vis(sig.is_private());

    let (builder_doc, surround_class_prefix) = make_extender_doc(sig, &extended_fn_name);

    let ExtenderReceiver {
        object_fn_param,
        object_param,
        object_arg,
    } = make_extender_receiver(sig);

    let Extender {
        builder_ty,
        builder_lifetime,
        builder_methods,
        builder_fields,
        builder_args,
        builder_inits,
    } = make_extender(
        sig.name(),
        object_fn_param,
        &required_fn_params,
        &default_fn_params,
    );

    // ExBuilder::new() constructor signature.
    let FnParamTokens {
        //params: required_params_builder_constructor,
        params: required_params_class_methods,
        func_general_lifetime,
        ..
    } = functions_common::make_params_exprs(
        required_fn_params.iter().cloned(),
        true,
        true,
        false,
        true,
    );
    let required_params_builder_constructor = &required_params_class_methods;

    // Forwarded args by some_function() and some_function_ex().
    let FnParamTokens {
        arg_names: required_args_internal,
        ..
    } = functions_common::make_params_exprs(
        required_fn_params.into_iter(),
        false,
        false,
        false,
        false,
    );

    let return_decl = &sig.return_value().decl;

    // If either the builder has a lifetime (non-static/global method), or one of its parameters is a reference,
    // then we need to annotate the _ex() function with an explicit lifetime. Also adjust &self -> &'a self.
    let receiver_self = &code.receiver.self_prefix;

    let (simple_receiver_param, extended_receiver_param, func_or_builder_lifetime);
    if let Some(func_lt) = func_general_lifetime.as_ref().or(builder_lifetime.as_ref()) {
        simple_receiver_param = &code.receiver.param;
        extended_receiver_param = &code.receiver.param_lifetime_a;
        func_or_builder_lifetime = Some(func_lt);
    } else {
        simple_receiver_param = &code.receiver.param;
        extended_receiver_param = &code.receiver.param;
        func_or_builder_lifetime = None;
    };

    // Technically, the builder would not need a lifetime -- it could just maintain an `object_ptr` copy.
    // However, this increases the risk that it is used out of place (not immediately for a default-param call).
    // Ideally we would require &mut, but then we would need `mut Gd<T>` objects everywhere.

    // #[allow] exceptions:
    // - wrong_self_convention:     to_*() and from_*() are taken from Godot
    // - redundant_field_names:     'value: value' is a possible initialization pattern
    // - needless-update:           Remainder expression '..self' has nothing left to change
    let builders = quote! {
        #[doc = #builder_doc]
        #[must_use]
        #cfg_attributes
        pub struct #builder_ty #builder_lifetime {
            // #builder_surround_ref
            #( #builder_fields, )*
        }

        #[allow(clippy::wrong_self_convention, clippy::redundant_field_names, clippy::needless_update)]
        impl #builder_lifetime #builder_ty #builder_lifetime {
            fn new(
                #object_param
                #( #required_params_builder_constructor, )*
            ) -> Self {
                Self {
                    #( #builder_inits, )*
                }
            }

            #( #builder_methods )*

            #[inline]
            pub fn done(self) #return_decl {
                #surround_class_prefix #full_fn_name(
                    #( #builder_args, )*
                )
            }
        }
    };

    let functions = quote! {
        // Simple function:
        // Lifetime is set if any parameter is a reference.
        #[doc = #default_parameter_usage]
        #[inline]
        #vis fn #simple_fn_name #func_general_lifetime  (
            #simple_receiver_param
            #( #required_params_class_methods, )*
        ) #return_decl {
            #receiver_self #extended_fn_name(
                #( #required_args_internal, )*
            ).done()
        }

        // _ex() function:
        // Lifetime is set if any parameter is a reference OR if the method is not static/global (and thus can refer to self).
        #[inline]
        #vis fn #extended_fn_name #func_or_builder_lifetime (
            #extended_receiver_param
            #( #required_params_class_methods, )*
        ) -> #builder_ty #builder_lifetime {
            #builder_ty::new(
                #object_arg
                #( #required_args_internal, )*
            )
        }
    };

    (functions, builders)
}

pub fn function_uses_default_params(sig: &dyn Function) -> bool {
    sig.params().iter().any(|arg| arg.default_value.is_some())
        && !special_cases::is_method_excluded_from_default_params(
            sig.surrounding_class(),
            sig.name(),
        )
}

// ----------------------------------------------------------------------------------------------------------------------------------------------
// Implementation

struct ExtenderReceiver {
    object_fn_param: Option<FnParam>,
    object_param: TokenStream,
    object_arg: TokenStream,
}

struct Extender {
    builder_ty: Ident,
    /// `None` if function is global/associated.
    builder_lifetime: Option<TokenStream>,
    builder_methods: Vec<TokenStream>,
    builder_fields: Vec<TokenStream>,
    builder_args: Vec<TokenStream>,
    builder_inits: Vec<TokenStream>,
}

#[derive(Copy, Clone, Debug)]
enum ExtenderFieldConversion {
    /// Regular type.
    None,

    /// Object argument.
    ObjectArg,

    /// Pass-by-ref type.
    Reference,

    /// Pass-by-ref type, but a default param means a value needs to be stored.
    ReferenceWithDefault,
}

fn make_extender_doc(sig: &dyn Function, extended_fn_name: &Ident) -> (String, TokenStream) {
    // Not in the above match, because this is true for both static/instance methods.
    // Static/instance is determined by first argument (always use fully qualified function call syntax).
    let surround_class_prefix;
    let builder_doc;

    match sig.surrounding_class() {
        Some(TyName { rust_ty, .. }) => {
            surround_class_prefix = quote! { re_export::#rust_ty:: };
            builder_doc = format!(
                "Default-param extender for [`{class}::{method}`][super::{class}::{method}].",
                class = rust_ty,
                method = extended_fn_name,
            );
        }
        None => {
            // There are currently no default parameters for utility functions
            // -> this is currently dead code, but _should_ work if Godot ever adds them.
            surround_class_prefix = TokenStream::new();
            builder_doc = format!(
                "Default-param extender for [`{function}`][super::{function}].",
                function = extended_fn_name
            );
        }
    };

    (builder_doc, surround_class_prefix)
}

fn make_extender_receiver(sig: &dyn Function) -> ExtenderReceiver {
    let builder_mut = match sig.qualifier() {
        FnQualifier::Const | FnQualifier::Static => quote! {},
        FnQualifier::Mut => quote! { mut },
        FnQualifier::Global => {
            unreachable!("default parameters not supported for global methods; {sig}")
        }
    };

    // Treat the object parameter like other parameters, as first in list.
    // Only add it if the method is not global or static.
    match sig.surrounding_class() {
        Some(surrounding_class) if !sig.qualifier().is_static_or_global() => {
            let class = &surrounding_class.rust_ty;

            ExtenderReceiver {
                object_fn_param: Some(FnParam {
                    name: ident("surround_object"),
                    type_: RustTy::ExtenderReceiver {
                        tokens: quote! { &'a #builder_mut re_export::#class },
                    },
                    default_value: None,
                }),
                object_param: quote! { surround_object: &'a #builder_mut re_export::#class, },
                object_arg: quote! { self, },
            }
        }
        _ => ExtenderReceiver {
            object_fn_param: None,
            object_param: TokenStream::new(),
            object_arg: TokenStream::new(),
        },
    }
}

fn make_extender(
    fn_name: &str,
    receiver_param: Option<FnParam>,
    required_params: &[&FnParam],
    default_params: &[&FnParam],
) -> Extender {
    // Note: could build a documentation string with default values here, but the Rust tokens are not very readable,
    // and often not helpful, such as Enum::from_ord(13). Maybe one day those could be resolved and curated.

    let all_fn_params = receiver_param
        .iter()
        .chain(required_params.iter().cloned())
        .chain(default_params.iter().cloned());
    let len = all_fn_params.size_hint().0;

    // If builder is a method with a receiver OR any *required* parameter is by-ref, use lifetime.
    // Default parameters cannot be by-ref, since they need to store a default value. Potential optimization later.
    let lifetime = if receiver_param.is_some() // !sig.qualifier().is_static_or_global()
        || required_params.iter().any(|p| p.type_.is_pass_by_ref())
    {
        Some(quote! { <'a> })
    } else {
        None
    };

    let mut result = Extender {
        builder_ty: format_ident!("Ex{}", conv::to_pascal_case(fn_name)),
        builder_lifetime: lifetime,
        builder_methods: Vec::with_capacity(default_params.len()),
        builder_fields: Vec::with_capacity(len),
        builder_args: Vec::with_capacity(len),
        builder_inits: Vec::with_capacity(len),
    };

    for param in all_fn_params {
        let FnParam {
            name,
            type_,
            default_value,
        } = param;

        let (field_type, conversion) = default_extender_field_decl(type_, default_value.is_some());

        // Initialize with default parameters where available, forward constructor args otherwise
        let init = if let Some(value) = default_value {
            make_field_init(name, Some(value), conversion)
        } else {
            make_field_init(name, None, conversion)
        };

        let builder_arg = match conversion {
            ExtenderFieldConversion::None => quote! { self.#name },
            ExtenderFieldConversion::ObjectArg => quote! { self.#name.cow_as_object_arg() },

            // Two cases:
            // * Param is required  ->  field type RefArg<'a, T>  ->  pass as-is.
            // * Param is default   ->  field type T              ->  pass as &self.
            ExtenderFieldConversion::ReferenceWithDefault => quote! { &self.#name },
            ExtenderFieldConversion::Reference => quote! { self.#name },
        };

        result.builder_fields.push(quote! { #name: #field_type });
        result.builder_args.push(builder_arg);
        result.builder_inits.push(init);
    }

    for param in default_params {
        let FnParam { name, type_, .. } = param;
        let param_type = type_.param_decl();
        let (_, field_needs_conversion) = default_extender_field_decl(type_, true);

        let field_init = make_field_init(name, Some(&quote! { value }), field_needs_conversion);

        let method = quote! {
            #[inline]
            pub fn #name(self, value: #param_type) -> Self {
                // Currently not testing whether the parameter was already set
                Self {
                    #field_init,
                    ..self
                }
            }
        };

        result.builder_methods.push(method);
    }

    result
}

/// Returns `( <field tokens>, <needs .consume_object()> )`.
fn default_extender_field_decl(
    ty: &RustTy,
    is_default_param: bool,
) -> (TokenStream, ExtenderFieldConversion) {
    match ty {
        RustTy::EngineClass { inner_class, .. } => {
            let cow_tokens = quote! { ObjectCow<crate::classes::#inner_class> };
            (cow_tokens, ExtenderFieldConversion::ObjectArg)
        }

        // Default parameters cannot be stored by reference, as they would need a backing storage. Alternative: Cow.
        ty if ty.is_pass_by_ref() => {
            if is_default_param {
                let ref_tokens = quote! { #ty };
                (ref_tokens, ExtenderFieldConversion::ReferenceWithDefault)
            } else {
                let ref_tokens = quote! { &'a #ty };
                (ref_tokens, ExtenderFieldConversion::Reference)
            }
        }

        other => (other.to_token_stream(), ExtenderFieldConversion::None),
    }
}

// Rewrite the above using match
fn make_field_init(
    name: &Ident,
    expr: Option<&TokenStream>, // None if the parameter has the same name as the field, and can use shorthand syntax.
    conversion: ExtenderFieldConversion,
) -> TokenStream {
    type Conv = ExtenderFieldConversion;

    match (conversion, expr) {
        (Conv::ObjectArg, Some(expr)) => quote! { #name: #expr.consume_object() },
        (Conv::ObjectArg, None) /*.             */ => quote! { #name: #name.consume_object() },

        // Currently no differentiation between None|Reference|ReferenceWithDefault; this may change...
        (Conv::None | Conv::Reference | Conv::ReferenceWithDefault, Some(expr)) => quote! { #name: #expr },
        (Conv::None | Conv::Reference | Conv::ReferenceWithDefault, None) /*.             */ => quote! { #name },
    }
}
